Imports System.IO
Imports System.Xml.Serialization
Imports Phoenix.Preferences.Events


''' <summary>
''' You don't need this class if you want to use Nap. You only need to care 
''' about this class if you want to create your own Preferences implementation.
''' In that case, derive your class from here, and override the storage-specific 
''' methods, that is, the methods whose names start with "Storage".
''' </summary>
Public MustInherit Class AbstractPreferencesNode
    Implements IPreferencesNode

    Protected Const PathSeparator As String = "/"
    Protected Const PathSeparatorChar As Char = "/"

    Protected ReadOnly mParent As AbstractPreferencesNode
    Protected ReadOnly mRoot As AbstractPreferencesNode
    Protected ReadOnly mIsSystemNode As Boolean
    Protected ReadOnly mName As String
    Protected ReadOnly mAbsolutePath As String
    Protected ReadOnly mChildren As Hashtable

    Private hasBeenRemoved As Boolean = False

    ''' <summary>
    ''' Creates a user preferences node. This constructor takes this node's parent,
    ''' and the name of this node. 
    ''' </summary>
    ''' <param name="parent">The parent node of this node. If this is a root node,
    ''' the parent must be null.</param>
    ''' <param name="name">The name of this node. If the parent is null, name
    ''' has to be an empty string (that is, ""). But if the parent is NOT null,
    ''' name cannot be an empty string. Name cannot be null either.</param>
    Protected Sub New(ByVal parent As AbstractPreferencesNode, ByVal name As String, ByVal isSystemNode As Boolean)
        '' name can never be null
        If IsNothing(name) Then
            Throw New ArgumentNullException("name", Res.APN_NameArgNull)
        End If

        '' initialize the children table
        If IsNothing(parent) Then
            '' if the parent is null, it's gotta be the root. Root cannot have a name.
            '' So name cannot be anything but an empty string
            If Not name.Equals(String.Empty) Then
                Throw New ArgumentException(Res.APN_RootArgEmpty, "name")
            End If
            '' so set this node as its own root
            Me.mRoot = Me
            '' and set the absolute path to "/" and the name, which is ""
            Me.mAbsolutePath = AbstractPreferencesNode.PathSeparator
        Else
            '' If parent is not null, then this is not a root node. So name 
            '' cannot be an empty string!
            If name.Equals(String.Empty) Then
                Throw New ArgumentException("Non-root node cannot have an empty string as its name.", "name")
            End If
            '' set this node's root to its parent's root.
            Me.mRoot = CType(parent.Root, AbstractPreferencesNode)
            '' and set the absolute path to whatever the parent's is, plus 
            '' this node's name. But remember that if the parent is root,
            '' its mAbsolutePath is "/" already, so no need to add
            '' a PathSeparator.
            If parent Is mRoot Then
                Me.mAbsolutePath = parent.AbsolutePath & name
            Else
                Me.mAbsolutePath = parent.AbsolutePath & PathSeparator & name
            End If
            '' add this node to its parent's list of children
            parent.mChildren(name) = Me
        End If
        Me.mIsSystemNode = isSystemNode
        Me.mChildren = New Hashtable(CaseInsensitiveHashCodeProvider.Default, CaseInsensitiveComparer.Default)
        Me.mParent = parent
        Me.mName = name
    End Sub

    ''' <summary>
    ''' The absolute path of a node. A root node's absolute path will be "/". 
    ''' </summary>
    Public ReadOnly Property AbsolutePath() As String Implements IPreferencesNode.AbsolutePath
        Get
            Return mAbsolutePath
        End Get
    End Property

    ''' <summary>
    ''' A string array containing the names of this node's children.
    ''' </summary>
    Public ReadOnly Property ChildrenNames() As String() Implements IPreferencesNode.ChildrenNames
        Get
            If hasBeenRemoved Then
                Throw New InvalidOperationException(Res.APN_NodeRemoved)
            End If

            Dim aChildrenNames As ArrayList = New ArrayList(Me.mChildren.Keys)
            Dim aSortedChildrenNames As String()
            aSortedChildrenNames = aChildrenNames.ToArray(Type.GetType("System.String"))
            Array.Sort(aSortedChildrenNames, CaseInsensitiveComparer.Default)
            Return aSortedChildrenNames
        End Get
    End Property

    Public ReadOnly Property Children() As IPreferencesNode() Implements IPreferencesNode.Children
        Get
            If (hasBeenRemoved) Then
                Throw New InvalidOperationException(Res.APN_NodeRemoved)
            End If

            Dim numberOfChildren As Integer = mChildren.Values.Count
            Dim childrenArray(numberOfChildren - 1) As IPreferencesNode

            mChildren.Values.CopyTo(childrenArray, 0)
            Return childrenArray
        End Get
    End Property

    Public Sub RemoveAllPreferences() Implements IPreferencesNode.RemoveAllPreferences
        If hasBeenRemoved Then
            Throw New InvalidOperationException(Res.APN_NodeRemoved)
        End If

        Try
            StorageRemoveAllPreferencesFromNode()
        Catch ex As Exception
            Throw New PreferencesStorageException(String.Format(Res.APN_RemoveAllPreferences, Me.Name), ex)
        End Try
    End Sub

    Public ReadOnly Property Root() As IPreferencesNode Implements IPreferencesNode.Root
        Get
            Return mRoot
        End Get
    End Property

    Public ReadOnly Property IsSystemNode() As Boolean Implements IPreferencesNode.IsSystemNode
        Get
            Return mIsSystemNode
        End Get
    End Property

    ''' <summary>
    ''' This utility method checks whether a pathName is valid (i.e.: no
    ''' consecutive slashes), and return a string of names, lower index is 
    ''' closer to the root. 
    ''' </summary>
    ''' <param name="pathName"></param>
    ''' <returns>true if pathName is absolute, false otherwise</returns>
    Private Function SplitPathAndReturnStartingNode(ByVal pathName As String, ByRef names As IList) As AbstractPreferencesNode
        Dim startingNode As AbstractPreferencesNode = Me

        names = New ArrayList(pathName.Split(PathSeparatorChar))
        '' a String.Empty in names[0] means that this is an absolute path, 
        '' that's OK. But a String.Empty anywhere between names[0] and
        '' names[length - 1] means that the path is illegal.

        If names(0).Equals(String.Empty) Then
            startingNode = Root
            names.RemoveAt(0)
        Else
            startingNode = Me
        End If


        Dim i As Integer = 0
        Dim j As Integer = names.Count
        For i = 0 To j - 1
            '' check, if there's a String.Empty anywhere, the path is illegal
            If names(i).Equals(String.Empty) Then
                Throw New ArgumentException(String.Format(Res.APN_PathNameSlash, pathName), "pathName")
            End If
        Next

        Return startingNode
    End Function


    Public Function GetNodeForPath(ByVal pathName As String) As IPreferencesNode Implements IPreferencesNode.GetNodeForPath
        If hasBeenRemoved Then Throw New InvalidOperationException(Res.APN_NodeRemoved)

        Try
            Dim names As IList
            Dim startingNode As AbstractPreferencesNode

            startingNode = splitPathAndReturnStartingNode(pathName, names)
            Return GetNodeRecursive(startingNode, names)
        Catch ex As ArgumentException
            Throw ex
        Catch ex As Exception
            Throw New PreferencesStorageException(String.Format(Res.APN_NodeCreate, pathName), ex)
        End Try
    End Function

    Private Function GetNodeRecursive(ByVal startingNode As AbstractPreferencesNode, ByVal names As IList) As AbstractPreferencesNode
        If names.Count > 0 Then
            '' see if it's already in the cache
            Dim name As String = CType(names(0), String)

            Dim temp As AbstractPreferencesNode = CType(startingNode.mChildren(name), AbstractPreferencesNode)
            '' if not, create it
            If IsNothing(temp) Then
                temp = CType(startingNode.StorageGetChildNodeForPath(name), AbstractPreferencesNode)
                startingNode.mChildren(name) = temp
                OnChildNodeAdded(New ChildNodeEventArgs(temp))
            End If
            startingNode = temp
            names.RemoveAt(0)
            Return startingNode.GetNodeRecursive(startingNode, names)
        End If
        Return Me
    End Function

    Public Function NodeExists(ByVal pathName As String) As Boolean Implements IPreferencesNode.NodeExists
        '' the root always exists!
        If pathName.Equals(PathSeparator) Then Return True

        '' if it's this node, see if it's been removed?
        If pathName.Equals(String.Empty) Then Return Not hasBeenRemoved

        '' if everything else, reject if it 
        '' this node has been removed.
        If (hasBeenRemoved) Then Throw New InvalidOperationException(Res.APN_NodeRemoved)

        '' now let's see...
        Dim names As IList
        Dim startingNode As AbstractPreferencesNode = SplitPathAndReturnStartingNode(pathName, names)
        Dim aNodeExists As Boolean = True
        For Each name As String In names
            startingNode = CType(startingNode.Children(name), AbstractPreferencesNode)
            If IsNothing(startingNode) Then
                aNodeExists = False
                Exit For
            End If
        Next
        Return aNodeExists
    End Function

    Public Sub SynchronizeWithStore() Implements IPreferencesNode.SynchronizeWithStore
        Try
            SynchronizeRecursive()
        Catch ex As Exception
            Throw New PreferencesStorageException(Res.APN_Synchonize, ex)
        End Try
    End Sub

    Private Sub SynchronizeRecursive()
        If hasBeenRemoved Then Throw New InvalidOperationException(Res.APN_NodeRemoved)

        For Each child As AbstractPreferencesNode In mChildren.Values
            child.SynchronizeRecursive()
        Next
        StorageSynchronize()
    End Sub

    Public Sub RemoveNode() Implements IPreferencesNode.RemoveNode
        Try
            If Me.Root Is Me Then Throw New InvalidOperationException(Res.APN_NodeRemoved)
            RemoveSubtree()

            '' after this, remove us from our parent's list of children
            Me.mParent.mChildren.Remove(Me.Name)
        Catch ex As Exception
            Throw New PreferencesStorageException(String.Format(Res.APN_NodeRemoved, Me.Name), ex)
        End Try
    End Sub

    Private Sub RemoveSubtree()
        '' it's not in RemoveNode, because this check needs to be done
        '' for every children below.
        If hasBeenRemoved Then Throw New InvalidOperationException(Res.APN_NodeRemoved)

        '' cause this method to be called on all
        '' the children of this node. Those calls,
        '' in turn, calls the same method on their
        '' children and so on.
        For Each child As AbstractPreferencesNode In mChildren.Values
            child.RemoveSubtree()
        Next

        '' since all the children has been removed, 
        '' clear the Hashtable
        mChildren.Clear()

        '' now remove ourselves from the storage
        '' and declare ourselves removed.
        StorageRemoveNode()
        hasBeenRemoved = True
        mParent.OnChildNodeRemoved(New ChildNodeEventArgs(Me))
    End Sub

    Public ReadOnly Property Keys() As String() Implements IPreferencesNode.Keys
        Get
            If hasBeenRemoved Then Throw New InvalidOperationException(Res.APN_NodeRemoved)
            Try
                Dim aKeys As String() = StorageKeys()
                Array.Sort(aKeys, CaseInsensitiveComparer.Default)
                Return aKeys
            Catch ex As Exception
                Throw New PreferencesStorageException(String.Format(Res.APN_GetKeys, Me.Name), ex)
            End Try
        End Get
    End Property

    Public ReadOnly Property Name() As String Implements IPreferencesNode.Name
        Get
            Return Name
        End Get
    End Property

    Public ReadOnly Property Parent() As IPreferencesNode Implements IPreferencesNode.Parent
        Get
            Return Me.Parent
        End Get
    End Property

    Public Function RemovePreference(ByVal key As String) As Boolean Implements IPreferencesNode.RemovePreference
        If (hasBeenRemoved) Then Throw New InvalidOperationException(Res.APN_NodeRemoved)

        Try
            Dim nodeToRemoveExists As Boolean = StorageRemovePreferenceFromNode(key)
            OnPreferenceChanged(New PreferenceChangeEventArgs(key, Nothing))
            Return nodeToRemoveExists
        Catch ex As Exception
            Throw New PreferencesStorageException(String.Format(Res.APN_KeyRemove, key, Me.Name), ex)
        End Try
    End Function

    '' all the get/set methods below.
    Public Function GetBoolean(ByVal key As String, ByVal DefaultValue As Boolean) As Boolean Implements IPreferencesNode.GetBoolean
        Try
            Dim val As String = GetString(key)
            If Not IsNothing(val) Then
                Return Boolean.Parse(val)
            End If
            Return DefaultValue
        Catch ex As Exception
            '' if we're here, something is wrong with
            '' our client's code. Raise a Debug.Fail.
            Debug.Fail("Error getting Boolean value from storage for key: " + key + ", returning default: " + DefaultValue, ex.StackTrace)
            Return DefaultValue
        End Try
    End Function

    Public Function GetByteArray(ByVal key As String, ByVal DefaultValue As Byte()) As Byte() Implements IPreferencesNode.GetByteArray
        If hasBeenRemoved Then Throw New InvalidOperationException(Res.APN_NodeRemoved)

        If IsNothing(key) Then Throw New ArgumentNullException("key", Res.APN_KeyNull)
        Try
            Dim val As Byte() = StorageGetBinary(key)
            If Not IsNothing(val) Then Return val
        Catch ex As Exception
            '' if we're here, something is wrong with
            '' our client's code. Raise a Debug.Fail.
            Debug.Fail("Error getting byte array value from storage for key: " + key, ex.StackTrace)
        End Try
        Return DefaultValue
    End Function

    Public Function GetDouble(ByVal key As String, ByVal DefaultValue As Double) As Double Implements IPreferencesNode.GetDouble
        Try
            Dim val As String = GetString(key)
            If Not IsNothing(val) Then Return Double.Parse(val)

            Return DefaultValue
        Catch ex As Exception
            '' if we're here, something is wrong with
            '' our client's code. Raise a Debug.Fail.
            Debug.Fail("Error getting Double value from storage for key: " + key + ", returning default: " + DefaultValue, ex.StackTrace)
            Return DefaultValue
        End Try
    End Function

    Public Function GetSingle(ByVal key As String, ByVal DefaultValue As Single) As Single Implements IPreferencesNode.GetSingle
        Try
            Dim val As String = GetString(key)
            If Not IsNothing(val) Then Return Single.Parse(val)

            Return DefaultValue
        Catch ex As Exception
            '' if we're here, something is wrong with
            '' our client's code. Raise a Debug.Fail.
            Debug.Fail("Error getting Single value from storage for key: " + key + ", returning default: " + DefaultValue, ex.StackTrace)
            Return DefaultValue
        End Try
    End Function

    Public Function GetInt32(ByVal key As String, ByVal DefaultValue As Int32) As Int32 Implements IPreferencesNode.GetInt32
        Try
            Dim val As String = GetString(key)
            If Not IsNothing(val) Then Return Int32.Parse(val)

            Return DefaultValue
        Catch ex As Exception
            '' if we're here, something is wrong with
            '' our client's code. Raise a Debug.Fail.
            Debug.Fail("Error getting Int32 value from storage for key: " + key + ", returning default: " + DefaultValue, ex.StackTrace)
            Return DefaultValue
        End Try
    End Function


    Public Function GetInt64(ByVal key As String, ByVal DefaultValue As Int64) As Int64 Implements IPreferencesNode.GetInt64
        Try
            Dim val As String = GetString(key)
            If Not IsNothing(val) Then Return Int64.Parse(val)

            Return DefaultValue
        Catch ex As Exception
            '' if we're here, something is wrong with
            '' our client's code. Raise a Debug.Fail.
            Debug.Fail("Error getting Int64 value from storage for key: " + key + ", returning default: " + DefaultValue, ex.StackTrace)
            Return DefaultValue
        End Try
    End Function

    Public Function GetDecimal(ByVal key As String, ByVal DefaultValue As Decimal) As Decimal Implements IPreferencesNode.GetDecimal
        Try
            Dim val As String = GetString(key)
            If Not IsNothing(val) Then Return Decimal.Parse(val)

            Return DefaultValue
        Catch ex As Exception
            '' if we're here, something is wrong with
            '' our client's code. Raise a Debug.Fail.
            Debug.Fail("Error getting Decimal value from storage for key: " + key + ", returning default: " + DefaultValue, ex.StackTrace)
            Return DefaultValue
        End Try
    End Function


    Public Function GetString(ByVal key As String, ByVal DefaultValue As String) As String Implements IPreferencesNode.GetString
        Try
            Dim val As String = GetString(key)
            If Not IsNothing(val) Then Return val

            Return DefaultValue
        Catch ex As Exception
            '' if we're here, something is wrong with
            '' our client's code. Raise a Debug.Fail.
            Debug.Fail("Error getting String value from storage for key: " + key + ", returning default: " + DefaultValue, ex.StackTrace)
            Return DefaultValue
        End Try
    End Function

    Private Function GetString(ByVal key As String) As String
        If hasBeenRemoved Then Throw New InvalidOperationException(Res.APN_NodeRemoved)
        If IsNothing(key) Then Throw New ArgumentNullException("key", Res.APN_KeyNull)
        Return StorageGetText(key)
    End Function

    Public Function GetSerializedObject(ByVal key As String, ByVal ObjectType As Type, ByVal DefaultValue As Object) As Object Implements IPreferencesNode.GetSerializedObject
        If hasBeenRemoved Then Throw New InvalidOperationException(Res.APN_NodeRemoved)

        If IsNothing(key) Then Throw New ArgumentNullException("key", Res.APN_KeyNull)

        Try
            Dim xs As XmlSerializer = New XmlSerializer(ObjectType)
            Dim val As Object = StorageGetSerializedObject(key, xs)
            If IsNothing(val) Then
                Return DefaultValue
            Else
                Return val
            End If
        Catch ex As Exception
            '' if we're here, something is wrong with
            '' our client's code. Raise a Debug.Fail.
            Debug.Fail("Error deserializing object from storage for key: " + key, ex.StackTrace)
            Return DefaultValue
        End Try
    End Function

    Default Public WriteOnly Property Item(ByVal key As String) As Object Implements IPreferencesNode.Item
        Set(ByVal Value As Object)
            If hasBeenRemoved Then Throw New InvalidOperationException(Res.APN_NodeRemoved)
            If IsNothing(key) Then Throw New ArgumentNullException("key", Res.APN_KeyNull)
            If IsNothing(Value) Then Throw New ArgumentNullException("value", Res.APN_ValueNull)

            Try
                If (TypeOf Value Is String) Or (TypeOf Value Is ValueType) Then
                    '' for value types, get the string representation
                    StoragePutText(key, Value.ToString())
                ElseIf (TypeOf Value Is Byte()) Then
                    '' if it's a byte array, they want this to be 
                    '' a binary one
                    StoragePutBinary(key, CType(Value, Byte()))
                Else
                    '' if it's neither a ValueType nor 
                    '' a byte array, so try to serialize 
                    '' this.
                    StoragePutSerializableObject(key, Value)
                End If
                OnPreferenceChanged(New PreferenceChangeEventArgs(key, Value))
            Catch ex As Exception
                Trace.WriteLine(ex.ToString)
                'Debug.Fail(ex.Message, ex.StackTrace)
            End Try
        End Set
    End Property

    '' For events
    Protected Overridable Sub OnChildNodeAdded(ByVal args As ChildNodeEventArgs)
        RaiseEvent ChildNodeAdded(Me, args)
    End Sub
    Public Event ChildNodeAdded As ChildNodeEventHandler Implements IPreferencesNode.ChildNodeAdded


    Protected Overridable Sub OnChildNodeRemoved(ByVal args As ChildNodeEventArgs)
        RaiseEvent ChildNodeRemoved(Me, args)
    End Sub
    Public Event ChildNodeRemoved As ChildNodeEventHandler Implements IPreferencesNode.ChildNodeRemoved

    Protected Overridable Sub OnPreferenceChanged(ByVal args As PreferenceChangeEventArgs)
        RaiseEvent PreferenceChanged(Me, args)
    End Sub
    Public Event PreferenceChanged As PreferenceChangeEventHandler Implements IPreferencesNode.PreferenceChanged

    '' to be overridden with storage-specific implementation class
    Protected MustOverride Function StorageGetText(ByVal key As String) As String
    Protected MustOverride Function StorageGetBinary(ByVal key As String) As Byte()
    Protected MustOverride Function StorageGetSerializedObject(ByVal key As String, ByVal serializer As XmlSerializer) As Object

    Protected MustOverride Sub StoragePutText(ByVal key As String, ByVal val As String)
    Protected MustOverride Sub StoragePutBinary(ByVal key As String, ByVal val As Byte())
    Protected MustOverride Sub StoragePutSerializableObject(ByVal key As String, ByVal val As Object)

    Protected MustOverride Sub StorageRemoveAllPreferencesFromNode()
    Protected MustOverride Function StorageGetChildNodeForPath(ByVal childNodeName As String) As IPreferencesNode
    Protected MustOverride Sub StorageSynchronize()
    Protected MustOverride Sub StorageRemoveNode()
    Protected MustOverride Function StorageKeys() As String()
    Protected MustOverride Function StorageRemovePreferenceFromNode(ByVal key As String) As Boolean
End Class